#include <cppunit/extensions/HelperMacros.h>

#include <boost/locale/encoding_utf.hpp>

using boost::locale::conv::utf_to_utf;

#include <iostream>
#include <map>

#include "szarpconfig_mocks.h"
#include "libSzarp2/include/config_info.h"
#include "libSzarp2/include/szarp_config.h"

namespace {
	std::string setToString(const std::set<std::string>& string_set, const std::string& separator = " ")
	{
		std::string msg;
		for(const auto& str : string_set)
		{
			msg += str + separator;
		}
		return msg;
	}

	std::string wstring_to_utf8(const std::wstring& str)
	{
		return utf_to_utf<char>(str.c_str(), str.c_str() + str.size());
	}
}

class TagsManagerTest: public CPPUNIT_NS::TestFixture
{
public:
	void testTagValidity();
	void testTagHolder();
	void testInvalidArguments();
	void testInvalidManager();
	void testSzarpConfig();

	CPPUNIT_TEST_SUITE( TagsManagerTest );
	CPPUNIT_TEST(testTagValidity);
	CPPUNIT_TEST(testTagHolder);
	CPPUNIT_TEST(testInvalidArguments);
	CPPUNIT_TEST(testSzarpConfig);
	CPPUNIT_TEST(testInvalidManager);
	CPPUNIT_TEST_SUITE_END();
};

CPPUNIT_TEST_SUITE_REGISTRATION( TagsManagerTest );

void TagsManagerTest::testTagHolder()
{
	using TagSet = std::set<std::string>;
	using test_pair = std::pair<std::string, TagSet>;
	const std::set<test_pair> test_set {
		{ "", {} },
		{ "+A", { "A" } },
		{ "+A, -A", {} },
		{ "+A, +B, +C, -A, -B", { "C" } },
		{ "+A, -B, -C, -D", { "A" } }
	};

	int test_counter = 0;
	for(const auto& test_case : test_set)
	{
		TTagHolder th;
		th.ParseTags(test_case.first);

		std::string msg = "Test case no. " + std::to_string(test_counter++) + " TagHolder tags: " + setToString(th.GetTags());
		msg += "\nExpected: " + setToString(test_case.second);

		int result = th.GetTags() == test_case.second ? 1 : 0;
		CPPUNIT_ASSERT_EQUAL_MESSAGE(msg, 1, result);
	}
}

void TagsManagerTest::testTagValidity()
{
	using test_tuple = std::tuple<std::string, std::string, bool>;
	const int MANAGER_TAGS = 0;
	const int HOLDER_TAGS = 1;
	const int RESULT = 2;

	std::set<test_tuple> test_set;
	test_set.emplace("A", "+A", true);
	test_set.emplace("A", "+A, -A", false);
	test_set.emplace("A, B", "+B", true);
	test_set.emplace("A", "+C", false);
	test_set.emplace("A", "", false);

	test_set.emplace("", "", true);
	test_set.emplace("", "+A", true);

	test_set.emplace("none", "", true);
	test_set.emplace("none", "+A,-A", true);
	test_set.emplace("none", "+A", false);
	test_set.emplace("none", "+A, +B, +C, -A, -B, -C", true);
	test_set.emplace("none, A", "+A", true);
	test_set.emplace("none, A", "+A, +B", true);
	test_set.emplace("none, A", "+B", false);

	test_set.emplace("all", "", true);
	test_set.emplace("all, A, B", "", true);
	test_set.emplace("all", "+A", true);
	test_set.emplace("all", "+A, -A", true);
	test_set.emplace("all, A, B", "+C", true);
	test_set.emplace("all, none", "+A", true);

	int test_counter = 0;
	for(const auto& test_case : test_set)
	{
		std::string manager_tags = std::get<MANAGER_TAGS>(test_case);
		std::string holder_tags = std::get<HOLDER_TAGS>(test_case);
		bool result = std::get<RESULT>(test_case);

		TagManager tag_manager;
		tag_manager.SetGlobalValidTags(manager_tags);

		TTagHolder th;
		th.ParseTags(holder_tags);

		int expected = result ? 1 : 0;
		int actual = tag_manager.HasValidTags(th) ? 1 : 0;

		std::string msg = "Test case no. " + std::to_string(test_counter++);
		msg += "\nManager tags: " + setToString(tag_manager.GetAllTags());
		msg += " tag holder tags: " + setToString(th.GetTags());

		CPPUNIT_ASSERT_EQUAL_MESSAGE(msg, expected, actual);
	}
}

void TagsManagerTest::testInvalidArguments()
{
	using test_tuple = std::pair<std::string, bool>;
	std::set<test_tuple> test_set;
	test_set.emplace("", true);
	test_set.emplace("+A", true);
	test_set.emplace("+", false);
	test_set.emplace("-B", true);
	test_set.emplace("-", false);
	test_set.emplace("A", false);
	test_set.emplace("++", false);
	test_set.emplace("--", false);
	test_set.emplace("+A+", false);
	test_set.emplace("+A A", false);
	test_set.emplace("+all", false);
	test_set.emplace("-all", false);
	test_set.emplace("+none", false);
	test_set.emplace("-none", false);
	test_set.emplace("+A,,+all,-none", false);

	int test_counter = 0;
	for(const auto& test_case : test_set)
	{
		int is_valid = test_case.second ? 1 : 0;
		std::string msg = "TestCase no. " + std::to_string(test_counter++);
		try {
			TTagHolder th;
			th.ParseTags(test_case.first);
			CPPUNIT_ASSERT_EQUAL_MESSAGE(msg, 1, is_valid);
		}
		catch(InvalidTagException& e){
			CPPUNIT_ASSERT_EQUAL_MESSAGE(msg, 0, is_valid);
		}
	}
}

void TagsManagerTest::testInvalidManager()
{
	// command line string, expected tags_manager tags, expected is_any_tag_valid, expected is_none_tag_valid
	using TagSet = std::set<std::string>;
	std::set<std::tuple<std::string, TagSet, bool, bool>> test_set;
	test_set.emplace("", TagSet(), true, true);
	test_set.emplace("all", TagSet(), true, false);
	test_set.emplace("none", TagSet(), false, true);
	test_set.emplace("all,none", TagSet(), true, true);
	test_set.emplace("a a", TagSet(), true, true);
	test_set.emplace("a a, A", TagSet({"A"}), false, false);
	test_set.emplace("a+a, A", TagSet({ "A" }), false, false);
	test_set.emplace("a-a, A", TagSet({ "A" }), false, false);
	test_set.emplace("a a, a+a, a-a, A", TagSet({ "A" }), false, false);
	test_set.emplace("a a, a+a, a-a", TagSet({}), true, true);
	test_set.emplace("none, A", TagSet({ "A" }), false, true);
	test_set.emplace("A, B, C", TagSet({ "A", "B", "C" }), false, false);
	test_set.emplace("A, B, C, none", TagSet({ "A", "B", "C" }), false, true);
	test_set.emplace("all, A", TagSet({ "A" }), true, false);

	for(const auto& test_case : test_set)
	{
		std::string cli_string = std::get<0>(test_case);
		TagSet expected_tags = std::get<1>(test_case);
		bool expected_any_tags = std::get<2>(test_case);
		bool expected_none_tags = std::get<3>(test_case);

		TagManager tag_manager;
		tag_manager.SetGlobalValidTags(cli_string);
		std::string msg = "Testing invalid manager tags with string: " + cli_string;
		msg += "\nExpecting: " + setToString(expected_tags);
		msg += expected_any_tags ? " and every tag valid" : "";
		msg += expected_none_tags ? " and none tags valid" : "";

		std::string fail_tags = "\nBut got actual tags: " + setToString(tag_manager.GetAllTags());
		std::string fail_any = "\nBut failed on any valid tag";
		std::string fail_none = "\nBut failed on none valid tag";

		int tag_result = expected_tags == tag_manager.GetAllTags() ? 1 : 0;
		int any_result = expected_any_tags == tag_manager.IsEveryParamValid() ? 1 : 0;
		int none_result = expected_none_tags == tag_manager.IsParamWithoutTagsValid() ? 1 : 0;

		CPPUNIT_ASSERT_EQUAL_MESSAGE(msg + fail_tags, 1, tag_result);
		CPPUNIT_ASSERT_EQUAL_MESSAGE(msg + fail_any, 1, any_result);
		CPPUNIT_ASSERT_EQUAL_MESSAGE(msg + fail_none, 1, none_result);
	}
}

void TagsManagerTest::testSzarpConfig()
{
	const std::string param_string = R"(<?xml version="1.0" encoding="utf-8"?>
<params xmlns="http://www.praterm.com.pl/SZARP/ipk" version="1.0" read_freq="10" send_freq="10" title="Globalmalt Bydgoszcz" documentation_base_url="http://www.szarp.com">
  <device xmlns:modbus="http://www.praterm.com.pl/SZARP/ipk-extra" daemon="/dev/null" tags="+A">
    <unit id="1" type="1" subtype="1" bufsize="1" tags="+B">
    	<param name="a:a:a" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="+C"/>
    	<param name="a:a:b" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="-A,-B"/>
    	<param name="a:a:c" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="-A"/>
    	<param name="a:a:d" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="-B"/>
    	<param name="a:a:e" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="-A, +C"/>
    	<param name="a:a:f" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="-A,-B, +C, +D, +E"/>
    	<param name="a:a:g" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" />
    	<param name="a:a:h" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags=""/>
    </unit>
	<unit id="2" type="1" subtype="1" bufsize="1" tags="-A">
    	<param name="a:b:a" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" />
    	<param name="a:b:b" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags=""/>
    	<param name="a:b:c" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="-A"/>
    	<param name="a:b:d" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="+B"/>
    	<param name="a:b:e" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="+A"/>
	</unit>
  </device>
  <device xmlns:modbus="http://www.praterm.com.pl/SZARP/ipk-extra" daemon="/dev/null">
    <unit id="1" type="1" subtype="1" bufsize="1">
    	<param name="b:a:a" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto"/>
    	<param name="b:a:b" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags=""/>
    	<param name="b:a:c" data_type="uinteger" short_name="-" unit="C" prec="1" draw_name="" base_ind="auto" tags="+A"/>
    </unit>
  </device>
</params>)";

	mocks::TSzarpConfigString tch(L"test");
	tch.readString(param_string);

	const std::map<std::wstring, std::set<std::string>> test_map {
		{ L"test:a:a:a", { "A", "B", "C" } },
		{ L"test:a:a:b", { } },
		{ L"test:a:a:c", { "B" } },
		{ L"test:a:a:d", { "A" } },
		{ L"test:a:a:e", { "B", "C" } },
		{ L"test:a:a:f", { "C", "D", "E" } },
		{ L"test:a:a:g", { "A", "B" } },
		{ L"test:a:a:h", { "A", "B" } },
		{ L"test:a:b:a", { } },
		{ L"test:a:b:b", { } },
		{ L"test:a:b:c", { } },
		{ L"test:a:b:d", { "B" } },
		{ L"test:a:b:e", { "A" } },
		{ L"test:b:a:a", { } },
		{ L"test:b:a:b", { } },
		{ L"test:b:a:c", { "A" } }
	};

	size_t test_counter = 0;
	for(TDevice* device = tch.getFirstDevice(); device != nullptr; device = device->GetNext())
	{
		for(TUnit* unit = device->GetFirstUnit(); unit != nullptr; unit = unit->GetNext())
		{
			for(TParam* param = unit->GetFirstParam(); param != nullptr; param = param->GetNext())
			{
				std::wstring name = param->GetGlobalName();
				std::string string_name = wstring_to_utf8(name);
				if(test_map.count(name) == 0)
					CPPUNIT_FAIL("Unknown param name " + string_name);

				std::string msg = "Test case no." + std::to_string(test_counter++);
				msg += "\nParam name: " + string_name;
				msg += "\nParam tags: " + setToString(param->GetTags());
				msg += "\nExpected tags: " + setToString(test_map.at(name));

				int result = param->GetTags() == test_map.at(name) ? 1 : 0;
				CPPUNIT_ASSERT_EQUAL_MESSAGE(msg, 1, result);
			}
		}
	}
	CPPUNIT_ASSERT_EQUAL_MESSAGE("Not every param was tested!", test_map.size(), test_counter);
}
